<?php

namespace App\Services;

use App\Http\Requests\Request;
use App\Models\Project;
use App\Repositories\ProjectRepository;
use App\Repositories\RecordRepository;
use App\Repositories\TaskRepository;
use App\Services\Task as DeployTask;
use App\Models\Task;
use Illuminate\Support\Facades\Auth;

class Deploy
{
    /**
     * 项目配置
     */
    protected $conf;

    /**
     * 上线任务配置
     */
    protected $task;

    /**
     * 高级任务
     */
    protected $deployTask;

    /**
     * 文件目录操作
     */
    protected $folder;

    /**
     * 初始化
     * Deploy constructor.
     * @param $taskId
     * @throws \Exception
     */
    public function __construct($taskId)
    {
        if (!$taskId) {
            throw new \Exception(trans("alerts.deploy.deployment_id_not_exists"));
        }

        $this->task = Task::find($taskId);
        if (!$this->task) {
            throw new \Exception(trans("alerts.deploy.deployment_id_not_exists"));
        }
        if ($this->task->user_id != Auth::id()) {
            throw new \Exception(trans("alerts.deploy.you_are_not_the_manager"));
        }
        // 项目配置
        $this->conf = ProjectRepository::getConf($this->task->project_id);
        $this->folder = new Folder($this->conf);
        $this->deployTask = new DeployTask($this->conf);
    }

    /**
     * 项目配置检测，提前发现配置不当之处。
     * @param $projectId
     * @return array
     */
    public static function Detection($projectId)
    {
        $project = ProjectRepository::getConf($projectId);
        // 宿主机检查
        $relt = Deploy::CheckHost($project);
        $code = $relt["flag"] ? 0 : -1;
        $log = $relt["log"];

        // 目标机器群检查
        $relt = Deploy::CheckTargets($project);
        $code = $relt["flag"] ? $code : -1;
        $log = array_merge($log, $relt["log"]);

        // task 检测todo...
        if ($code === 0) {
            $log[] = trans("alerts.deploy.project_configuration_works");
        }
        return ["log" => join("<br>", $log), "code" => $code];
    }

    /**
     * 宿主机检查(1、权限 2、git)
     *
     * @param array $project
     * @return array
     */
    public static function CheckHost($project)
    {
        $result = true;
        $log = [];
        // 本地git ssh-key是否加入deploy-keys列表
        $revision = new Git($project);
        try {
            // 1.检测宿主机检出目录是否可读写
            $codeBaseDir = ProjectRepository::_getDeployFromDir($project);
            $isWritable = is_dir($codeBaseDir) ? is_writable($codeBaseDir) : @mkdir($codeBaseDir, 0755, true);
            if (!$isWritable) {
                $result = false;
                $log[] = langFmt(trans('alerts.deploy.hosted_server_is_not_writable_error'), [
                    'user' => getenv("USER"),
                    'path' => $project["deploy_from"],
                ]);
            }

            // 2.检测宿主机ssh是否加入git信任
            $ret = $revision->updateRepo("master", $codeBaseDir);
            if (!$ret) {
                $result = false;
                $error = langFmt(trans('alerts.deploy.ssh-key_to_git'),
                    ['user' => getenv("USER")]);
                $log[] = langFmt(trans("alerts.deploy.hosted_server_ssh_error"), [
                    'error' => $error,
                ]);
            }
        } catch (\Exception $e) {
            $result = false;
            $log[] = langFmt(trans("alerts.deploy.hosted_server_sys_error"), [
                'error' => $e->getMessage()
            ]);
        }
        return ["flag" => $result, "log" => $log];
    }

    /**
     * 目标机器群检查
     *
     * @param array $project
     * @return array
     */
    public static function CheckTargets($project)
    {
        $result = true;
        $log = [];
        // 权限与免密码登录检测
        $deployTask = new DeployTask($project);
        try {
            // 检测php用户是否加入目标机ssh信任
            $command = 'id';
            $ret = $deployTask->runRemoteTaskCommandPackage([$command]);
            if (!$ret) {
                $result = false;
                $log[] = langFmt(trans("alerts.deploy.target_server_ssh_error"), [
                    'local_user' => getenv("USER"),
                    'remote_user' => $project["release_user"],
                    'path' => $project["release_to"],
                ]);
            }

            // 检测php用户是否具有目标机release目录读写权限
            $tmpDir = 'detection' . time();
            $command = sprintf('mkdir -p %s', ProjectRepository::getReleaseVersionDir($tmpDir));
            $ret = $deployTask->runRemoteTaskCommandPackage([$command]);
            if (!$ret) {
                $result = false;
                $log[] = langFmt(trans("alerts.deploy.target_server_is_not_writable_error"), [
                    'remote_user' => $project["release_user"],
                    'path' => $project["release_library"],
                ]);
            }

            // 清除
            $command = sprintf('rm -rf %s', ProjectRepository::getReleaseVersionDir($tmpDir));
            $deployTask->runRemoteTaskCommandPackage([$command]);
        } catch (\Exception $e) {
            $result = false;
            $log[] = langFmt(trans("alerts.deploy.target_server_sys_error"), [
                'error' => $e->getMessage()
            ]);
        }

        return ["flag" => $result, "log" => $log];
    }

    /**
     * 发起上线
     * @throws \Exception
     */
    public function StartDeploy()
    {
        // 任务失败或者审核通过时可发起上线
        if (!TaskRepository::canDeploy($this->task->status)) {
            throw new \Exception(trans("alerts.deploy.deployment_only_done_for_once"));
        }
        // 清除历史记录
        RecordRepository::deleteAllTask($this->task->id);

        try {
            // 目标机器群检查
            $relt = Deploy::CheckTargets($this->conf);
            if ($relt["flag"] == false) {
                throw new \Exception(join("<br>", $relt["log"]));
            }

            //开始部署/回滚
            if ($this->task->action == config("constants.task.action.new")) {
                $this->_makeVersion();
                $this->_initWorkspace();
                $this->_preDeploy();
                $this->_revisionUpdate();
                $this->_postDeploy();
                $this->_transmission();
                $this->_updateRemoteServers($this->task->link_id);
                $this->_cleanRemoteReleaseVersion();
                $this->_cleanUpLocal($this->task->link_id);
            } else {
                $this->_rollback($this->task->ex_link_id);
            }

            /** 至此已经发布版本到线上了，需要做一些记录工作 */

            // 记录此次上线的版本（软链号）和上线之前的版本
            ///对于回滚的任务不记录线上版本
            if ($this->task->action == config("constants.task.action.new")) {
                $this->task->ex_link_id = $this->conf["version"];
            }
            // 第一次上线的任务不能回滚、回滚的任务不能再回滚
            if ($this->task->action == config("constants.task.action.rollback") || TaskRepository::projectTaskCount($this->task->project_id) == 1) {
                $this->task->enable_rollback = config("constants.task.enable_rollback.ban");
            }
            $this->task->status = config("constants.task.status.complete");
            $this->task->save();

            // 可回滚的版本设置
            $this->_enableRollBack();

            // 记录当前线上版本（软链）回滚则是回滚的版本，上线为新版本
            $project = Project::find($this->conf["id"]);
            $project->version = $this->task->link_id;
            $project->save();
        } catch (\Exception $e) {
            $this->task->status = config("constants.task.status.fail");
            $this->task->save();
            // 清理本地部署空间
            $this->_cleanUpLocal($this->task->link_id);
            throw $e;
        }
    }

    /**
     * 产生一个上线版本
     */
    private function _makeVersion()
    {
        $version = date("Ymd-His", time());
        $this->task->link_id = $version;

        return $this->task->save();
    }

    /**
     * 部署后置触发任务
     * git代码检出之后，可能做一些调整处理，如vendor拷贝，配置环境适配（mv config-test.php config.php）
     *
     * @return bool
     * @throws \Exception
     */
    private function _postDeploy()
    {
        $sTime = Command::getMs();
        $ret = $this->deployTask->postDeploy($this->task->link_id);
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        RecordRepository::saveRecord($this->deployTask, $this->task->id, config("constants.record.action.post_deploy"), $duration);

        if (!$ret) {
            throw new \Exception(trans("alerts.deploy.post_deploy_task_error"));
        }

        return true;
    }

    /**
     * 传输文件/目录到指定目标机器
     *
     * @return bool
     * @throws \Exception
     */
    private function _transmission()
    {
        $sTime = Command::getMs();

        // 循环 scp
        $this->folder->scpCopyFiles($this->conf, $this->task);

        // 记录执行时间
        $duration = Command::getMs() - $sTime;

        RecordRepository::saveRecord($this->folder, $this->task->id, config("constants.record.action.sync"), $duration);

        return true;
    }

    /**
     * 执行远程服务器任务集合
     * 对于目标机器更多的时候是一台机器完成一组命令，而不是每条命令逐台机器执行
     * @param $version
     * @param int $delay 每台机器延迟执行post_release任务间隔, 不推荐使用, 仅当业务无法平滑重启时使用
     * @return bool
     * @throws \Exception
     */
    private function _updateRemoteServers($version, $delay = 0)
    {
        $cmd = [];
        // pre-release task
        if (($preRelease = DeployTask::getRemoteTaskCommand($this->conf["pre_release"], $version))) {
            $cmd[] = $preRelease;
        }
        // link
        if (($linkCmd = $this->folder->getLinkCommand($version))) {
            $cmd[] = $linkCmd;
        }
        // post-release task
        if (($postRelease = DeployTask::getRemoteTaskCommand($this->conf["post_release"], $version))) {
            $cmd[] = $postRelease;
        }

        $sTime = Command::getMs();
        // run the task package
        $ret = $this->deployTask->runRemoteTaskCommandPackage($cmd, $delay);
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        RecordRepository::saveRecord($this->deployTask, $this->task->id, config("constants.record.action.update_remote"), $duration);
        if (!$ret) {
            throw new \Exception(trans("alerts.deploy.update_servers_error"));
        }

        return true;
    }

    /**
     * 可回滚的版本设置
     *
     * @return int
     */
    private function _enableRollBack()
    {
        $task = new Task();
        $task = $task->where("status", config("constants.task.status.complete"))->where("project_id", $this->task->project_id);
        $offset = $task
            ->select('id')
            ->orderBy("id", "desc")
            ->offset($this->conf["keep_version_num"])
            ->value('id');
        if (!$offset) {
            return true;
        }
        return $task->where("id", "<=", $offset)->update(['enable_rollback' => config("constants.task.enable_rollback.ban")]);
    }

    /**
     * 检查目录和权限，工作空间的准备
     * 每一个版本都单独开辟一个工作空间，防止代码污染
     *
     * @return bool
     * @throws \Exception
     */
    private function _initWorkspace()
    {
        $sTime = Command::getMs();

        // 本地宿主机工作区初始化
        $this->folder->initLocalWorkspace($this->task);

        // 远程目标目录检查，并且生成版本目录
        $ret = $this->folder->initRemoteVersion($this->task->link_id);
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        RecordRepository::saveRecord($this->folder, $this->task->id, config("constants.record.action.permission"), $duration);

        if (!$ret) {
            throw new \Exception(trans("alerts.deploy.init_deployment_workspace_error"));
        }

        return true;
    }

    /**
     * 部署前置触发任务
     * 在部署代码之前的准备工作，如git的一些前置检查、vendor的安装（更新）
     *
     * @return bool
     * @throws \Exception
     */
    private function _preDeploy()
    {
        $sTime = Command::getMs();
        $ret = $this->deployTask->preDeploy($this->task->link_id);
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        RecordRepository::saveRecord($this->deployTask, $this->task->id, config("constants.record.action.pre_deploy"), $duration);

        if (!$ret) {
            throw new \Exception(trans("alerts.deploy.pre_deploy_task_error"));
        }

        return true;
    }

    /**
     * 更新代码文件
     *
     * @return bool
     * @throws \Exception
     */
    private function _revisionUpdate()
    {
        // 更新代码文件
        $revision = new Git($this->conf);
        $sTime = Command::getMs();
        $ret = $revision->updateToVersion($this->task); // 更新到指定版本
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        RecordRepository::saveRecord($this->deployTask, $this->task->id, config("constants.record.action.clone"), $duration);

        if (!$ret) {
            throw new \Exception(trans("alerts.deploy.update_code_error"));
        }

        return true;
    }


    /**
     * 只保留最大版本数，其余删除过老版本
     */
    private function _cleanRemoteReleaseVersion()
    {
        return $this->deployTask->cleanUpReleasesVersion();
    }

    /**
     * 执行远程服务器任务集合回滚，只操作pre-release、link、post-release任务
     * @param $version
     * @return bool
     */
    private function _rollback($version)
    {
        return $this->_updateRemoteServers($version);
    }

    /**
     * 收尾工作，清除宿主机的临时部署空间
     * @param null $version
     * @return bool
     */
    private function _cleanUpLocal($version = null)
    {
        // 创建链接指向
        $this->folder->cleanUpLocal($version);
        return true;
    }

    /**
     * 生成回滚任务
     * @throws \Exception
     */
    public function Rollback()
    {
        if ($this->task->ex_link_id == $this->task->link_id) {
            throw new \Exception(trans("alerts.task.no_rollback_twice"));
        }
        $conf = Project::where("id", $this->task->project_id)->where("status", config("constants.project.status.active"))->first();
        if (!$conf) {
            throw new \Exception(trans("alerts.task.can_not_rollback_the_closed_project_job"));
        }

        $rollbackTask = $this->task->replicate();
        $rollbackTask->status = config("constants.task.status.new");
        $rollbackTask->action = config("constants.task.action.rollback");
        $rollbackTask->link_id = $this->task->ex_link_id;
        $rollbackTask->title = $this->task->title . ' - ' . trans("crud.rollback");
        if ($rollbackTask->save()) {
            return $rollbackTask->id;
        } else {
            throw new \Exception(trans("alerts.task.create_a_rollback_job_failed"));
        }
    }
}